#ifndef SHARED_POINTER_H
#define SHARED_POINTER_H
#include <functional>
#include <iostream>

namespace cp5
{

    struct Delete
    {
        template <typename T>
        auto operator()(T *p) const
        {
            std::cout << "delete " << *p << std::endl;
            delete p;
        }
    };

    template <typename T>
    class SharedPointer;

    template <typename T>
    auto swap(SharedPointer<T> &lhs, SharedPointer<T> &rhs)
    {
        using std::swap;
        swap(lhs.ptr, rhs.ptr);
        swap(lhs.ref_count, rhs.ref_count);
        swap(lhs.deleter, rhs.deleter);
    }

    template <typename T>
    class SharedPointer
    {
    public:
        //
        //  Default Ctor
        //
        SharedPointer()
            : ptr{nullptr}, ref_count{new std::size_t(1)}, deleter{cp5::Delete{}}
        {
        }
        //
        //  Ctor that takes raw pointer
        //
        explicit SharedPointer(T *raw_ptr)
            : ptr{raw_ptr}, ref_count{new std::size_t(1)}, deleter{cp5::Delete{}}
        {
        }
        //
        //  Copy Ctor
        //
        SharedPointer(SharedPointer const &other)
            : ptr{other.ptr}, ref_count{other.ref_count}, deleter{other.deleter}
        {
            ++*ref_count;
        }
        //
        //  Move Ctor
        //
        SharedPointer(SharedPointer &&other) noexcept
            : ptr{other.ptr}, ref_count{other.ref_count}, deleter{std::move(other.deleter)}
        {
            other.ptr = nullptr;
            other.ref_count = nullptr;
        }
        //
        //  Copy assignment
        //
        SharedPointer &operator=(SharedPointer const &rhs)
        {
            // increment first to ensure safty for self-assignment
            ++*rhs.ref_count;
            decrement_and_destroy();
            ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter;
            return *this;
        }
        //
        //  Move assignment
        //
        SharedPointer &operator=(SharedPointer &&rhs) noexcept
        {
            cp5::swap(*this, rhs);
            rhs.decrement_and_destroy();
            return *this;
        }
        //
        //  Conversion operator
        //
        operator bool() const
        {
            return ptr ? true : false;
        }
        //
        //  Dereference
        //
        T &operator*() const
        {
            return *ptr;
        }
        //
        //  Arrow
        //
        T *operator->() const
        {
            return &*ptr;
        }
        //
        //  Use count
        //
        auto use_count() const
        {
            return *ref_count;
        }
        //
        //  Get underlying pointer
        //
        auto get() const
        {
            return ptr;
        }
        //
        //  Check if the unique user
        //
        auto unique() const
        {
            return 1 == *ref_count;
        }
        //
        //  Swap
        //
        auto swap(SharedPointer &rhs)
        {
            cp5::swap(*this, rhs);
        }
        //
        // Free the object pointed to, if unique
        //
        auto reset()
        {
            decrement_and_destroy();
        }
        //
        // Reset with the new raw pointer
        //
        auto reset(T *pointer)
        {
            if (ptr != pointer)
            {
                decrement_and_destroy();
                ptr = pointer;
                ref_count = new std::size_t(1);
            }
        }
        //
        //  Reset with raw pointer and deleter
        //
        auto reset(T *pointer, const std::function<void(T *)> &d)
        {
            reset(pointer);
            deleter = d;
        }
        //
        //  Dtor
        //
        ~SharedPointer()
        {
            decrement_and_destroy();
        }

    private:
        T *ptr;
        std::size_t *ref_count;
        std::function<void(T *)> deleter;

        auto decrement_and_destroy()
        {
            if (ptr && 0 == --*ref_count)
                delete ref_count,
                    deleter(ptr);
            else if (!ptr)
                delete ref_count;
            ref_count = nullptr;
            ptr = nullptr;
        }
    };
} // namespace
#endif // SHARED_POINTER_H
